【IT168 技术】在上一篇文章(.NET中的异步编程:传统的异步编程)中我们围观了传统的异步编程,感受到了异步编程不是简单的事情。传统的异步方式将本来紧凑的代码都分成两部分,不仅仅降低了代码的可读性,还让一些基本的程序构造无法使用,所以大部分开发人员在遇到应该使用异步的地方都忍痛割爱。本来我在本篇文章中想讨论一下.NET世界中已有的几个辅助异步开发的类库,但是经过思考后觉得在这之前介绍一下一些理论知识也许对理解后面的类库以及更新的内容有所帮助。今天我们要讨论的是Continuation Passing Style,简称CPS。
CPS
首先,我们看看下面这个方法:
2: {
3: return a + b;
4: }
我们一般这样调用它:
2:
3: public void Print(int result)
4: {
5: Console.WriteLine(result);
6: }
如果我们以CPS的方式编写上面的代码则是这个样子:
2: {
3: continueWith(a+b);
4: }
5:
6: Add(5, 6, (ret) => Print(ret));
就好像我们将方法倒过来,我们不再是直接返回方法的结果;我们现在做的是接受一个委托,这个委托表示我这个方法运算完后要干什么,就是传说的continue。对于这里来说,Add的continue就是Print。
不仅是上面这样的代码示例。在一个方法中,在本语句后面执行的语句都可以称之为本语句的continue。
CPS 与 Async
那么可能有人要问,你说这么多跟异步有什么关系么?对,跟异步有很大的关系。回想上一篇文章,经典的异步模式都是一个以Begin开头的方法发起异步请求,并且向这个方法传入一个回调(callback),当异步执行完毕后该回调会被执行,那么我们可以称该回调为这个异步请求的continue:
这又有什么用呢?那先来看看我们期望写出什么样子的异步代码吧(注意,这是伪代码,不要没有看文章就直接粘贴代码到vs运行):
2: var asyncResult1 = request.BeginGetResponse(...);
3: var response = request.EndGetResponse(asyncResult1);
4: using(stream = response.GetResponseStream())
5: {
6: var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
7: var actualRead = stream.EndRead(asyncResult2);
8: }
对,我们想要像同步的方式一样编写异步代码,我讨厌那么多回调,特别是一环嵌套一环的回调。
参照前面对CPS的讨论,在request.BeginGetResponse之后的代码,都是它的continue,如果我能够有一种机制获得我的continue,然后在我执行完毕之后调用continue该多好啊。可惜,C#没有像Scheme那样的控制操作符call/cc获取continue。
思路貌似到这儿断了。但是我们是否可以换个角度想想,如果我们能给上面这段代码加上标识:在每个异步请求发起的地方都加一个标识,而标识之后的部分就是continue。
标识1 var asyncResult1 = request.BeginGetResponse(...);
var response = request.EndGetResponse(asyncResult1);
using(stream = response.GetResponseStream())
{
标识2 var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
var actualRead = stream.EndRead(asyncResult2);
}
当执行到 标识1 时,立即返回,并且记住本次执行只执行到了 标识1,当异步请求完毕后,它知道上次执行到了 标识1,那么这个时候就从标识1的下一行开始执行,当执行到标识2时,又遇到一个异步请求,立即返回并记住本次执行到了标识2,然后请求完毕后从标识2的下一行恢复执行。那么现在的任务就是如果打标识以及在异步请求完毕后如何从标识位置开始恢复执行。